Conversation
A binding holds native memory and the process that finds out later is the user's. The suite here cannot see that: a test that closes nothing and asserts on a message passes, and what it left behind is somebody else's problem an hour into a run. So the allocator is asked instead. A driver in the tck opens and closes every handle this client hands out, failures beside successes, and scripts/leaks.sh runs it with LeakSanitizer ahead of the JVM and reads the report for blocks the engine allocated and nobody gave back. The narrow question is the whole trick. A JVM does not free at exit, on purpose, so pointing a leak checker at one that does nothing at all reports about a megabyte in several thousand allocations and none of it is anything a caller can act on. What is answerable is whether any unfreed block came out of libzu, and a leak record carries the stack it was allocated from, so it is answerable by reading frame #1. Frame #1 rather than any frame, because of what the JNI row turned up. Asking for a jmethodID allocates a JVM-side table entry the JVM never frees, and the stack for it runs through the shim because the shim is what asked. Any-frame matching called seven of those ours and they are not: at #1 they are os::malloc in libjvm. A block the shim really did allocate has the shim at #1 and is still caught. The count of records let through is printed rather than dropped quietly. The gate runs first and has to fail. A report with no libzu in it looks the same whether nothing leaked, the sanitizer was never loaded, the library was never called, or the driver died early, and three of those four are green for the wrong reason. So the driver is run once with ZU_LEAK_GATE=1, which drops a database, a connection, a statement, a result, an appender and a frame on the floor, and the script stops if that comes back clean. On server3, both providers: the gate leaks 57 records naming zu_execute, zu_appender_open and zu_database_open, and the clean run is 0 of 545 on Panama and 0 of 390 on JNI, against a JVM whose own report is a megabyte either way. The full reactor is green beside it, 199 tck cases and 12 Arrow. Linux only. LeakSanitizer does not exist on macOS, and what covers the same ground there is the lifecycle half of the misuse suite, which counts open file descriptors either side of a few hundred failures and needs no allocator to agree with it.
tamnd
added a commit
to tamnd/zu
that referenced
this pull request
Aug 22, 2026
…ens and closes every handle the client hands out, run under LeakSanitizer with the report filtered to blocks the engine allocated, on both providers, with a gate that leaks on purpose first. (#595) On server3 the gate leaks 57 records naming `zu_execute`, `zu_appender_open` and `zu_database_open`, and the clean run is 0 of 545 on Panama and 0 of 390 on JNI, against a JVM whose own unfreed megabyte is deliberately not read. That takes zu-java to 90 on practice, which is the tier 1 threshold, and makes it the fourth client at its tier. What is left there is `api-map` and `perf`, which are reports the release collects rather than apparatus, and zu-python, zu-node and zu-go are missing the same two. | Client | Practice | Missing | |---|---|---| | zu-python | 90 | `api-map`, `perf` | | zu-node | 90 | `api-map`, `perf` | | zu-go | 90 | `api-map`, `perf` | | zu-java | 90 | `api-map`, `perf` | | zu-c | 0 | everything | `docs/clients/overview.md` is regenerated with `cargo run -q -p xtask -- clients` rather than edited. BODY zu-java holds leaks tamnd/zu-java#16 landed the leaks item, so the row moves from 80 to 90 on practice, which is the tier 1 threshold. A driver in the tck opens and closes every handle the client hands out, failures beside successes, under LeakSanitizer, and the report is read for blocks whose stack names libzu at frame #1. The gate runs first and drops one of everything on the floor, because a report with no libzu in it looks the same whether nothing leaked or the sanitizer was never loaded. Both providers on server3: 57 records when the gate leaks, 0 of 545 on Panama and 0 of 390 on JNI when it does not. docs/clients/overview.md is regenerated rather than edited.
22 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
leaksitem off the clients scorecard, which is the last piece of apparatus this client owes. Sanitizer and leak jobs in CI, because a binding holds native memory and the process that finds out later is the user's.Why the suite cannot answer this
A test that closes nothing and asserts on a message passes. There is no
Cleanerbehind this client and nothing waits for a collector, which the API documents rather than apologises for, so a handle that is dropped stays open until the process ends and no allocator in Java will ever call it garbage: the pointer is still reachable from a live object. That makes an allocator outside the JVM the only thing that can see the mistake.The narrow question
Pointing LeakSanitizer at a JVM that does nothing at all reports about a megabyte in several thousand allocations. All of it is the JVM's own, and it is not a bug: an exiting process has an operating system to give its pages back, and a shutdown that walks them is time spent for nobody.
So the question this asks is not "did anything leak". It is "of the blocks nobody freed, is any of them one the engine allocated", which is answerable, because a leak record carries the stack it came from.
Frame #1, and what the JNI row taught
Matching any frame in the stack is wrong, and the JNI provider is where that shows. Asking for a jmethodID allocates a JVM-side table entry the JVM never frees, and the stack for it runs through the shim, because the shim is what asked. Any-frame matching called seven of those ours on a clean run. At frame #1 they are
os::mallocin libjvm, and frame #1 is whoever called malloc, since #0 is the sanitizer's own interceptor.A block the shim really did allocate has the shim at #1 and is still caught, which is why the shim is named in the filter at all. The count of records let through is printed on every run rather than dropped quietly, so a decision this script made is a decision somebody can argue with.
The gate
A report with no libzu in it looks the same whether nothing leaked, the sanitizer was never loaded, the library was never called, or the driver exited early. Three of those four are green for the wrong reason.
So the driver runs first with
ZU_LEAK_GATE=1, which drops a database, a connection, a statement, a result, an appender and a frame on the floor on purpose, and the script stops if that comes back clean. This is the same shape as zu-go'sinternal/leakgate, run first for the same reason.What it did on server3
Both providers, against
libzubuilt from the engine at HEAD:The gate records name
zu_execute,zu_appender_openandzu_database_open. The full reactor is green beside all of it: 199 tck cases on Panama, 12 Arrow,BUILD SUCCESS.What is in here
zudb-tck/src/main/java/dev/zudb/tck/Leaks.javais the driver, and it lives in the tck because both providers owe the same answer. It runs an in-memory database with a frame the engine reads where it lies, a database on disk with a prepared statement, three appenders that end three different ways, a transaction and a duplicated connection, then the same handles on the paths that never reach the end: a syntax error, an undefined variable, a column read as the type it is not, a row past the end, a value the appender refuses. The failures are the half that matters, because a close that is never reached is the shape a leak has here.scripts/leaks.shfinds the runtime and the symbolizer, builds the driver and one provider, and runs the gate and then the clean pass.ASAN_OPTIONSturns offhandle_segvand allows a user handler, because a JVM uses the faults its own handlers catch as ordinary control flow and a sanitizer that takes those first turns a working JVM into a crash on the first query.The CI job runs both providers on Linux. Linux only: LeakSanitizer does not exist on macOS, and what covers the same ground on the darwin rows is the lifecycle half of the misuse suite, which counts open file descriptors either side of a few hundred failures and needs no allocator to agree with it.
The engine is not built with the sanitizer, deliberately. Interposing the allocator is enough to see a block nobody freed, and an instrumented engine would mean building the whole of Rust twice to answer a question about this repository.
The scorecard
That is 80 to 90 on practice for this client, which is the tier 1 threshold. What is left is
api-mapandperf, which are reports the release collects rather than apparatus, and zu-python, zu-node and zu-go are missing the same two.